<?php

/**
 * @see Workset_Sphinx_Adapter_Abstract
 */
require_once 'Workset/Sphinx/Adapter/Abstract.php';


class Workset_Sphinx_Adapter_SphinxQL extends Workset_Sphinx_Adapter_Abstract {

    const META_VAR_NAME = 'Variable_name';

    const META_VAR_VALUE = 'Value';

    const STRIP_QUOTES = 1;

    const NO_QUOTES = 2;

    const QUOTES = 3;

    protected $_port = 9306;

    protected $_host = 'localhost';

    protected $_username = '';

    protected $_password = '';

    protected $_connection = null;

    protected $_query = null;

    protected $_meta = null;

    public function query(Workset_Sphinx_Query_Interface $query) {
        if ($query instanceof Workset_Sphinx_Query_Expr) {
            $query = $query->getExpression();
            $query = $this->_quote($query);
        } else {
            $query = $this->_buildQuery($query);
        }

        $profilerQuery = null;
        $profiler = $this->getProfiler();

        try {

            if (null === $profiler) {
                return $this->_getConnection()->query($query);
            }

            $profilerQuery = $profiler->queryStart($query);

            $result = $this->_getConnection()->query($query);

            if (!$result instanceof PDOStatement) {
                throw new PDOException('PDO query error');
            }

            $profiler->queryEnd($profilerQuery);

            return $result;

        } catch (Exception $e) {
            if (null !== $profiler) {
                $profiler->queryEnd($profilerQuery);
            }

            throw new Workset_Sphinx_Adapter_Exception('SphinxQL query error. Original exception: '.$e->getMessage());
        }
    }

    public function getMeta() {
        try {

            $this->_meta = null;

            $meta = $this->_getConnection()->query('SHOW META');

            if ($meta) {
                $rows = array();

                foreach ($meta->fetchAll(PDO::FETCH_ASSOC) as $row) {

                    $key = $row[self::META_VAR_NAME];

                    if (false !== ($pos = strpos($key, '['))) {
                        $name = substr($key, 0, $pos);
                        $index = (int)substr($key, $pos + 1);

                        if (!isset($rows[$name])) {
                           $rows[$name] = array();
                        }

                        $rows[$name][$index] = $row[self::META_VAR_VALUE];
                    } else {
                        $rows[$key] = $row[self::META_VAR_VALUE];
                    }
                }

                $this->_meta = $rows;

            }

            return $this->_meta;

        }  catch (Exception $e) {
            throw new Workset_Sphinx_Adapter_Exception('SphinxQL query error. Original exception: '.$e->getMessage());
        }
    }

    public function getTotal() {
        if (null === $this->_meta) {
            $meta = $this->getMeta();
        } else {
            $meta = $this->_meta;
        }

        if ($meta && isset($meta['total'])) {
            return (int)$meta['total'];
        }
    }

    protected function _buildQuery(Workset_Sphinx_Query_Interface $query) {
        $result = '';

        $expr = array();

        foreach ($query->getSelect() as $key => $value) {
            if ($value != '*') {
                if (is_string($key)) {
                    $expr[] = $value . ' as ' . $key;
                    continue;
                }
            }

            $expr[] = $value;
        }

        $expr = implode(', ', $expr);

        $indexes = implode(', ', $query->getIndexes());

        $matches = $query->getMatches();

        if ($matches) {
            if (count($matches) > 1 || !is_int(key($matches))) {

                $keys = array_keys($matches);
                $values = array_values($matches);

                $keys = array_map(function($val) {
                    if (false === strpos($val, '@')) {
                        $val = '@' . $val;
                    }
                    return $val;
                }, $keys);

                $matches = array();

                foreach ($keys as $index => $key) {
                   $matches[] = $key . ' ' . $this->_quote($values[$index]);
                }
            }

            $matches = 'MATCH(\'' . $this->_quote( implode(' ', $matches) ) . '\')';
        }

        $attributes = array();

        foreach ($query->getAttributes() as $key => $value) {
            if (is_array($value)) {
                $cur = $this;
                $value = array_map(function($v) use($cur) {
                    return $cur->quote($v, true);
                }, $value);

                $value = implode(',', $value);
            } else {
                $value = $this->_quote($value, self::QUOTES);
            }

            $attributes[] = str_replace('?', $value, $key);
        }

        $attributes = implode(' AND ', $attributes);

        $order = array();

        foreach ($query->getOrder() as $key => $value) {
            $order[] = $key . ' ' . $value;
        }

        $order = implode(',', $order);

        $groupBy = $query->getGroupBy();

        //Currently only supports grouping by a single column.
        if (count($groupBy) > 1) {
           $groupBy = array( current($groupBy) );
        }

        $groupBy = implode(',', $groupBy);

        $limit = $query->getLimit();

        $offset = $query->getOffset();

        $options = array();

        foreach ($query->getOptions() as $key => $value) {

            if (is_array($value)) {
                $value = '('. http_build_query($value, null, ', ') .')';
            }

           $options[] = $key . '=' . $value;
        }

        if (!$expr || !$indexes) {
           throw new Workset_Sphinx_Adapter_Exception('Invalid SphinxQL query');
        }

        //build query string
        $result = 'SELECT '.$expr.' FROM '.$indexes;

        if ($matches || $attributes) {
            $result .= ' WHERE ';
            $where = array();

            if ($matches) {
               $where[] = $matches;
            }

            if ($attributes) {
               $where[] = $attributes;
            }

            $result .= implode(' AND ', $where);
        }

        if ($groupBy) {
            $result .= ' GROUP BY ' . $groupBy;
        }

        if ($order) {
            $result .= ' ORDER BY ' . $order;
        }

        if ($limit) {
            $result .= " LIMIT $offset, $limit";
        }

        if ($options) {
            $result .= " OPTION ". implode(', ', $options);
        }

        //echo $result;

        return $result;
    }

    public function quote($value, $handleQuote = self::STRIP_QUOTES) {
        return $this->_quote($value, $handleQuote);
    }

    /**
     * @see Zend_Db_Adapter_Abstract::_quote
     * @param  $value
     * @return string
     */
    protected function _quote($value, $handleQuote = self::STRIP_QUOTES)
    {
        if (is_int($value)) {
            return $value;
        } elseif (is_float($value)) {
            return sprintf('%F', $value);
        }

        if ($handleQuote == self::STRIP_QUOTES) {
            $value = preg_replace('/[\'\"]+/', '', $value);
        }

        $value = addcslashes($value, "\000\n\r\\'\"\032");

        if ($handleQuote == self::QUOTES) {
            $value =  "'" . $value . "'";
        }

        return $value;
    }

    public function setOptions($options) {
        if (!is_array($options)) {
            if ($options instanceof Zend_Config) {
                $options = $options->toArray();
            } else {
                $options = (array)$options;
            }
        }

        foreach ($options as $key => $value) {
            switch ($key) {
                case 'port':
                      $this->setPort($value);
                break;

                case 'host':
                      $this->setHost($value);
                break;

                case 'username':
                      $this->setUsername($value);
                break;

                case 'password':
                      $this->setPassword($value);
                break;

                case 'indexes':
                      $this->getQuery()->setIndexes($value);
                break;

                case 'limit':
                      $this->getQuery()->setLimit($value);
                break;

                case 'offset':
                      $this->getQuery()->setOffset($value);
                break;

                case 'attributes':
                      $this->getQuery()->setAttributes($value);
                break;

                case 'options':
                      $this->getQuery()->setOptions($value);
                break;

                case 'profiler':
                      $this->setProfiler($value);
                break;
            }
        }
    }

    public function setPort($number) {
        if ($number > 0 && $number < 65536) {
            $this->_port = (int)$number;
        } else {
            throw new Workset_Sphinx_Adapter_Exception('Invalid number of port');
        }
    }

    public function setHost($host) {
        $this->_host = (string)$host;
    }

    public function setUsername($name) {
        $this->_username = (string)$name;
    }

    public function setPassword($password) {
        $this->_password = (string)$password;
    }

    public function getQuery() {
        if (null === $this->_query) {
           $this->_query = new Workset_Sphinx_Query();
        }

        return $this->_query;
    }

    public function getNewQuery() {
        $q = clone $this->getQuery();
        return $q;
    }

    protected function _getConnection() {
        if (null === $this->_connection) {

            try {

                $this->_connection = new PDO("mysql:host={$this->_host};port={$this->_port}", $this->_username, $this->_password);

            } catch (PDOException $e) {
                throw new Workset_Sphinx_Adapter_Exception('Cannot connect to sphinx using PDO. Original message:' . $e->getMessage());
            }

        }

        return $this->_connection;
    }

    protected function _resetConnection() {
        $this->_connection = null;
    }

}